// Distributed under an MIT license: http://codemirror.net/LICENSE

/**
 * freemarker
 */

;(function (mod) {
  if (typeof exports == 'object' && typeof module == 'object')
    // CommonJS
    mod(require('codemirror'))
  else if (typeof define == 'function' && define.amd)
    // AMD
    define(['codemirror'], mod)
  // Plain browser env
  else mod(CodeMirror)
})(function (CodeMirror) {
  'use strict'

  CodeMirror.defineMode('freemarker', function (config) {
    'use strict'

    // our default settings; check to see if they're overridden
    var settings = {
      leftDelimiter: '<',
      rightDelimiter: '>',
      tagSyntax: 1,
      // 1 angle_bracket,2 square_bracket
    }
    if (config.hasOwnProperty('tagSyntax')) {
      if (config.tagSyntax === 2) {
        settings.tagSyntax = 2
        settings.leftDelimiter = '['
        settings.rightDelimiter = ']'
      }
    }

    var keyFunctions = [
      'assign',
      'attempt',
      'autoesc',
      'break',
      'case',
      'compress',
      'default',
      'else',
      'elseif',
      'escape',
      'fallback',
      'function',
      'flush',
      'ftl',
      'global',
      'if',
      'import',
      'include',
      'items',
      'list',
      'local',
      'lt',
      'macro',
      'nested',
      'noautoesc',
      'noescape',
      'noparse',
      'nt',
      'outputformat',
      'recover',
      'recurse',
      'return',
      'rt',
      'sep',
      'setting',
      'stop',
      'switch',
      't',
      'visit',
    ]
    var specialVariables = [
      'auto_esc',
      'caller_template_name',
      'current_template_name',
      'data_model',
      'error',
      'get_optional_template',
      'globals',
      'lang',
      'locale',
      'locale_object',
      'locals',
      'main',
      'main_template_name',
      'namespace',
      'node',
      'now',
      'output_encoding ',
      'output_format',
      'template_name',
      'time_zone',
      'url_escaping_charset',
      'vars',
      'version',
    ]

    var freemarkerStartTagArray = ['#', '@']

    var freemarkerEndTagArray = ['/#', '/@', '/>']

    var last
    var freemarkerMode
    var regs = {
      operatorChars: /[+\-*&%=<>!?:;,|&]/,
      validIdentifier: /[a-zA-Z0-9_]/,
      stringChar: /['"]/,
    }

    var helpers = {
      cont: function (style, lastType, lastFreemarkerMode) {
        last = lastType
        freemarkerMode = lastFreemarkerMode
        return style
      },
      chain: function (stream, state, parser) {
        state.tokenize = parser
        return parser(stream, state)
      },
    }

    // our various parsers
    var parsers = {
      // the main tokenizer
      tokenizer: function (stream, state) {
        if (stream.match(settings.leftDelimiter, true)) {
          if (stream.match('#--', true)) {
            return helpers.chain(
              stream,
              state,
              parsers.inBlock('comment', '--' + settings.rightDelimiter),
            )
          } else {
            for (var i = 0; i < freemarkerStartTagArray.length; i++) {
              if (stream.match(freemarkerStartTagArray[i], false)) {
                state.tokenize = parsers.freemarkerTemplate
                if (freemarkerStartTagArray[i] == '@') {
                  freemarkerMode = 'macro'
                } else {
                  freemarkerMode = 'tag'
                }
                last = 'startTag'
                return 'tag'
              }
            }
            for (var i = 0; i < freemarkerEndTagArray.length; i++) {
              if (stream.match(freemarkerEndTagArray[i], false)) {
                state.tokenize = parsers.freemarkerTemplate
                if (freemarkerEndTagArray[i] == '/@') {
                  freemarkerMode = 'macro'
                } else {
                  freemarkerMode = 'tag'
                }
                last = 'endTag'
                return 'tag'
              }
            }
          }
        } else if (stream.match('${', false)) {
          state.tokenize = parsers.freemarkerTemplate
          last = 'startTag'
          freemarkerMode = 'echo'
          return 'keyword'
        }
        stream.next()
        return null
      },

      // parsing freemarker content
      freemarkerTemplate: function (stream, state) {
        if (stream.match(settings.rightDelimiter, true)) {
          state.depth--
          if (state.depth <= 0) {
            state.tokenize = parsers.tokenizer
          }
          return helpers.cont('tag', null, null)
        } else if ('echo' == state.freemarkerMode && stream.match('}', true)) {
          state.depth--
          if (state.depth <= 0) {
            state.tokenize = parsers.tokenizer
          }
          return helpers.cont('keyword', null, null)
        }

        if (stream.match(settings.leftDelimiter, true)) {
          for (var i = 0; i < freemarkerStartTagArray.length; i++) {
            if (stream.match(freemarkerStartTagArray[i], false)) {
              state.depth++
              if (freemarkerStartTagArray[i] == '@') {
                return helpers.cont('tag', 'startTag', 'macro')
              } else {
                return helpers.cont('tag', 'startTag', 'tag')
              }
            }
          }
          for (var i = 0; i < freemarkerEndTagArray.length; i++) {
            if (stream.match(freemarkerEndTagArray[i], false)) {
              state.depth++
              if (freemarkerEndTagArray[i] == '/@') {
                return helpers.cont('tag', 'endTag', 'macro')
              } else {
                return helpers.cont('tag', 'endTag', 'tag')
              }
            }
          }
        } else if (stream.match('${', true)) {
          state.depth++
          return helpers.cont('keyword', 'startTag', 'echo')
        }
        var ch = stream.next()
        if ('.' == ch) {
          if (
            'echo' == state.freemarkerMode ||
            'whitespace' == state.last ||
            'operator' == state.last
          ) {
            for (var i = 0; i < specialVariables.length; i++) {
              if (stream.match(specialVariables[i], true)) {
                return helpers.cont('keyword', 'variable', state.freemarkerMode)
              }
            }
          }
          if ('keyword' == state.last && stream.eatWhile(regs.validIdentifier)) {
            return helpers.cont('keyword', null, state.freemarkerMode)
          } else {
            return helpers.cont('operator', 'childVariable', state.freemarkerMode)
          }
        } else if (regs.stringChar.test(ch)) {
          state.tokenize = parsers.inAttribute(ch)
          return helpers.cont('string', 'string', state.freemarkerMode)
        } else if (regs.operatorChars.test(ch)) {
          if ('?' === ch) {
            return helpers.cont('operator', 'builtin', state.freemarkerMode)
          } else {
            return helpers.cont('operator', 'operator', state.freemarkerMode)
          }
        } else if ('[' == ch || '{' == ch || '(' == ch) {
          return helpers.cont('bracket', 'bracket', state.freemarkerMode)
        } else if (']' == ch || '}' == ch || ')' == ch) {
          return helpers.cont('bracket', 'variable', state.freemarkerMode)
        } else if ('/' == ch) {
          return helpers.cont('tag', 'endTag', state.freemarkerMode)
        } else if ('@' == ch && 'macro' == state.freemarkerMode) {
          stream.eatWhile(regs.validIdentifier)
          return helpers.cont('keyword', 'keyword', state.freemarkerMode)
        } else if (/\d/.test(ch)) {
          stream.eat(/x/i)
          stream.eatWhile(/\d/)
          return helpers.cont('number', 'number', state.freemarkerMode)
        } else if (
          'tag' == state.freemarkerMode &&
          'whitespace' == state.last &&
          (stream.match('as', true) ||
            stream.match('in', true) ||
            stream.match('using', true))
        ) {
          return helpers.cont('keyword', 'operator', state.freemarkerMode)
        } else if (
          'tag' == state.freemarkerMode &&
          'whitespace' == state.last &&
          (stream.match('gte', true) ||
            stream.match('lte', true) ||
            stream.match('gt', true) ||
            stream.match('lt', true))
        ) {
          return helpers.cont('operator', 'operator', state.freemarkerMode)
        } else {
          if ('builtin' == state.last) {
            stream.eat('?')
            stream.eatWhile(regs.validIdentifier)
            return helpers.cont('builtin', 'variable', state.freemarkerMode)
          } else if ('whitespace' == state.last || 'bracket' == state.last) {
            if ('macro' == state.freemarkerMode) {
              stream.eatWhile(regs.validIdentifier)
              return helpers.cont('attribute', 'attribute', state.freemarkerMode)
            } else {
              stream.eatWhile(regs.validIdentifier)
              return helpers.cont('variable-2', 'variable', state.freemarkerMode)
            }
          } else if ('operator' == state.last) {
            stream.eatWhile(regs.validIdentifier)
            return helpers.cont('variable-2', 'variable', state.freemarkerMode)
          } else if ('childVariable' == state.last) {
            stream.eatWhile(regs.validIdentifier)
            return helpers.cont('variable-3', 'variable', state.freemarkerMode)
          } else if (/\s/.test(ch)) {
            last = 'whitespace'
            return null
          } else if ('string' == state.last) {
            stream.eatWhile(regs.validIdentifier)
            return helpers.cont('attribute', 'attribute', state.freemarkerMode)
          } else {
            if ('startTag' == state.last || 'endTag' == state.last) {
              if ('echo' == state.freemarkerMode) {
                stream.eatWhile(regs.validIdentifier)
                return helpers.cont('variable-2', 'variable', state.freemarkerMode)
              }
            }
            if ('tag' == state.freemarkerMode) {
              var str = ''
              if (ch != '/') {
                str += ch
              }
              var c = null
              while ((c = stream.eat(regs.validIdentifier))) {
                str += c
              }
              for (var i = 0; i < keyFunctions.length; i++) {
                if ('#' + keyFunctions[i] == str) {
                  return helpers.cont('keyword', 'keyword', state.freemarkerMode)
                }
              }
            }
          }
          return helpers.cont('error', 'tag', state.freemarkerMode)
        }
      },

      inAttribute: function (quote) {
        return function (stream, state) {
          var prevChar = null
          var currChar = null
          while (!stream.eol()) {
            currChar = stream.peek()
            if (stream.next() == quote && '\\' !== prevChar) {
              state.tokenize = parsers.freemarkerTemplate
              break
            }
            prevChar = currChar
          }
          return 'string'
        }
      },

      inBlock: function (style, terminator) {
        return function (stream, state) {
          while (!stream.eol()) {
            if (stream.match(terminator)) {
              state.tokenize = parsers.tokenizer
              break
            }
            stream.next()
          }
          return style
        }
      },
    }

    // the public API for CodeMirror
    return {
      startState: function () {
        return {
          tokenize: parsers.tokenizer,
          mode: 'freemarker',
          last: null,
          freemarkerMode: null,
          depth: 0,
        }
      },
      token: function (stream, state) {
        state.last = last
        state.freemarkerMode = freemarkerMode
        return state.tokenize(stream, state)
      },
      electricChars: '',
    }
  })

  CodeMirror.defineMIME('text/freemarker', 'freemarker')
})
